﻿// PortableMediator
// (c) Lance Wynn - http://PortableMediator.codeplex.com/
// License: MIT (http://www.opensource.org/licenses/mit-license.php)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Reflection;

namespace PortableMediator
{
    public class Mediator : IMediator
    {
        /// <summary>
        /// Stores registered Tokens, and their associated ParameterTypes, and lists of subscribed actions
        /// </summary>
        private Dictionary<object, Dictionary<Type, IList<IWeakAction>>> tokenIndex = new Dictionary<object, Dictionary<Type, IList<IWeakAction>>>();

        /// <summary>
        /// Stores types with their associated interface chains, and baseclass chains, this is so we can easily figure out 
        /// assignability without reflecting each time.
        /// </summary>
        private Dictionary<Type, List<Type>> assignableTypesCache = new Dictionary<Type, List<Type>>();

        public async Task RegisterSubscriber<TParam>(object token, Action<TParam> action)
        {
            await RegisterSubscriber(token, action.Target, action.GetMethodInfo());
        }

        public async Task RegisterSubscriber<TParam, TResult>(object token, object source, Func<TParam, TResult> func)
        {
            await RegisterSubscriber(token, source, func.GetMethodInfo());
        }

        public async Task RegisterSubscriber(object token, object target, string methodName)
        {
            Type TargetType = target.GetType();
            var methodInfo = TargetType.GetExposedMethods(methodName);
            if (methodInfo == null)///if there is no methodInfo for the given Name, check for a property setter method
            {
                methodInfo = TargetType.GetExposedProperties(methodName).SetMethod;
            }
            await RegisterSubscriber(token, target, methodInfo);
        }

        public async Task RegisterSubscriber(object token, object target, MethodInfo methodInfo)
        {
            var parameterType = methodInfo.GetParameters()[0];
            var returnType = methodInfo.ReturnType;

            if (returnType == typeof(void))
            {
                returnType = typeof(object);
            }

            var genericType = typeof(MediatorFunc<>);
            var mediatorFuncType = genericType.MakeGenericType(new Type[] { returnType });

            var mediatorFunc = Activator.CreateInstance(mediatorFuncType, target, methodInfo);
            await AddTargetToIndexes((IWeakAction)mediatorFunc, token);
        }

        public async Task RegisterSubscriber(object target)
        {
            await Task.Run(async () =>
            {
                var targetType = target.GetType();
                var props = targetType.GetExposedProperties();
                var meths = targetType.GetExposedMethods();
                foreach (var meth in meths)
                {
                    foreach (var sink in meth.GetCustomAttributes<MediatorMessageSinkAttribute>())
                    {
                        await this.RegisterSubscriber(sink.Token, target, meth.Name);
                    }
                }

                foreach (var prop in props)
                {
                    foreach (var sink in prop.GetCustomAttributes<MediatorMessageSinkAttribute>())
                    {
                        await this.RegisterSubscriber(sink.Token, target, prop.Name);
                    }
                }
            });
        }

        public async Task PublishMessage<TParam>(object token, TParam param)
        {
            await this.PublishMessage<TParam, object>(token, param, null);
        }

        public async Task PublishMessage<TParam, TResult>(object token, TParam param, Action<TResult> callback)
        {
            var actionList = await BuildActionListForType(token, typeof(TParam));

            foreach (var action in actionList)
            {
                if (action.IsAlive())
                {
                    await action.Invoke(param, callback);
                }
                else
                {
                    tokenIndex[token][typeof(TParam)].Remove(action);
                }
            }

            ////Publish that the message was published.
            if (token is string && (string)token != PortableMediator.MediatorTokens.StandardMediatorTokens.TokenRegistered && (string)token != PortableMediator.MediatorTokens.StandardMediatorTokens.MessagePublished)
            {
                var publishedArgs = new MessagePublishedArgs(token, param, actionList.Count);
                await PublishMessage(PortableMediator.MediatorTokens.StandardMediatorTokens.MessagePublished, publishedArgs);
            }
        }

        public async Task<IEnumerable<TResult>> PublishMessageWithReturnResult<TParam, TResult>(object token, TParam param)
        {
            TResult result = (TResult)(typeof(TResult).GetTypeInfo().IsValueType ? Activator.CreateInstance(typeof(TResult)) : null);
            var resultList = new List<TResult>();
            await PublishMessage<TParam, TResult>(token, param, (p) => { resultList.Add(p); });
            return resultList;
        }

        public async Task<IList<IWeakAction>> BuildActionListForType(object token, Type paramType)
        {
            var actionList = new List<IWeakAction>();

            await Task.Run(async () =>
            {
                var assignableTypeList = await GetCachedAssignableTypeList(paramType);
                if (tokenIndex.ContainsKey(token))
                {
                    foreach (var assignableType in assignableTypeList)
                    {
                        if (tokenIndex[token].ContainsKey(assignableType))
                        {
                            actionList.AddRange(tokenIndex[token][assignableType]);
                        }
                    }
                }
            });
            return actionList;
        }

        private async Task<List<Type>> GetCachedAssignableTypeList(Type type)
        {
            if (assignableTypesCache.ContainsKey(type))
            {
                return assignableTypesCache[type];
            }

            var assignableTypes = await BuildAssignableTypeList(type);

            lock (assignableTypesCache)
            {
                if (assignableTypesCache.ContainsKey(type))
                {
                    return assignableTypesCache[type];
                }

                assignableTypesCache.Add(type, assignableTypes);
            }

            return assignableTypes;
        }

        private async Task<List<Type>> BuildAssignableTypeList(Type type, List<Type> workingList = null)
        {
            if (workingList == null)
            {
                workingList = new List<Type>();
            }
            workingList.Add(type);

            foreach (var inf in type.GetTypeInfo().ImplementedInterfaces)
            {
                await BuildAssignableTypeList(inf, workingList);
            }

            if (type.GetTypeInfo().BaseType != null)
            {
                await BuildAssignableTypeList(type.GetTypeInfo().BaseType, workingList);
            }

            return workingList;
        }

        private async Task AddTargetToIndexes(IWeakAction action, object token)
        {
            await Task.Run(async () =>
            {
                var actionType = await action.GetParamType();
                await AddTargetToIndexes(action, token, actionType);
            });
        }

        private async Task AddTargetToIndexes(IWeakAction action, object token, Type paramTypeToAdd)
        {
            await Task.Run(() =>
            {
                var actionType = paramTypeToAdd;
                lock (tokenIndex)
                {
                    if (!tokenIndex.ContainsKey(token))
                    {
                        tokenIndex.Add(token, new Dictionary<Type, IList<IWeakAction>>());
                    }

                    var typeList = tokenIndex[token];

                    if (!typeList.ContainsKey(actionType))
                    {
                        typeList.Add(actionType, new List<IWeakAction>());
                    }

                    typeList[actionType].Add(action);
                }
            });
            if (token is string && token != PortableMediator.MediatorTokens.StandardMediatorTokens.TokenRegistered && token != PortableMediator.MediatorTokens.StandardMediatorTokens.MessagePublished)
            {
                await PublishMessage(PortableMediator.MediatorTokens.StandardMediatorTokens.TokenRegistered, token);
            }
        }
    }
}
